<script setup>
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
// 显示提示词插件
import ChatPrompt from "@/components/ChatPrompt.vue";
// 显示大模型消息回复插件
import ChatReply from "@/components/ChatReply.vue";
import {Delete, Edit, InfoFilled, More, Plus, Promotion, Search, Share, VideoPause} from '@element-plus/icons-vue'
import 'highlight.js/styles/a11y-dark.css'
import {isMobile, randString, removeArrayItem, UUID} from "@/utils/libs.js";
import {ElMessage, ElMessageBox} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router";
import Clipboard from "clipboard";
import {checkSession, getClientId, getSystemInfo} from "@/store/cache";
// 欢迎插件
import Welcome from "@/components/Welcome.vue";
import {useSharedStore} from "@/store/sharedata";
// 文件选择组件
import FileSelect from "@/components/FileSelect.vue";
// 聊天文件列表组件
import FileList from "@/components/FileList.vue";
// 聊天设置组件
import ChatSetting from "@/components/ChatSetting.vue";
import BackTop from "@/components/BackTop.vue";
import {showMessageError, closeLoading, showLoading} from "@/utils/dialog";
import RealtimeConversation from "@/components/RealtimeConversation.vue";
import MarkdownIt from "markdown-it";
import {full as emoji} from 'markdown-it-emoji'

const title = ref('GeekAI-智能助手')
// 模型列表
const models = ref([])
// 当前选中模型Id
const modelId = ref(0)
// 会话的消息列表
const chatData = ref([])
// 完整会话列表
const allChats = ref([])
// 搜索过的会话列表，如果没有搜索 chatList 和 allChats 数据一致
const chatList = ref(allChats.value)
// 主窗口高度
const mainWinHeight = ref(0)
// 聊天内容框高度
const chatBoxHeight = ref(0)
// 侧边会话列表高度
const leftBoxHeight = ref(0)

const loading = ref(false)
// 登录用户信息
const loginUser = ref(null)
// 应用角色列表
const roles = ref([])
const router = useRouter()
// 当前应用角色Id
const roleId = ref(0)
// 当前选择话Id
const chatId = ref()
// 新开会话
const newChatItem = ref(null)
// 是否登录
const isLogin = ref(false)
// 是否显示欢迎页面
const showHello = ref(true)
// 输入框引用
const inputRef = ref(null)
const textHeightRef = ref(null)
// 公告弹框是否显示
const showNotice = ref(false)
// 公告内容
const notice = ref('')
// 存储公告的 KEY
const noticeKey = ref("SYSTEM_NOTICE")
const store = useSharedStore()
// 提示词输入框行数
const row = ref(1)
// 聊天配置弹框控制属性
const showChatSetting = ref(false)
// 聊天列表样式
const listStyle = ref(store.chatListStyle)
const config = ref({advance_voice_power: 0})
const voiceChatUrl = ref('')

// 监听聊天样式是否发生变化
watch(() => store.chatListStyle, (newVal) => {
  listStyle.value = newVal
})
// 工具列表
const tools = ref([])
// 选中的工具列表
const toolSelected = ref([])
// 是否使用流式输出
const stream = ref(store.chatStream)
// 监听是否启用实时聊天流
watch(() => store.chatStream, (newVal) => {
  stream.value = newVal
})

// 判断是否是手机端
if (isMobile()) {
  router.push('/mobile/chat')
}

// 初始化角色Id参数
// router.currentRoute.value.query.role_id 获取URL中通过查询字符串传递的参数role_id（如?role_id=123）
if (router.currentRoute.value.query.role_id) {
  roleId.value = parseInt(router.currentRoute.value.query.role_id)
}

// 初始化会话Id
// router.currentRoute.value.params.id 获取URL中通过动态路由定义的参数id（如路径/user/:id中的id值）
chatId.value = router.currentRoute.value.params.id
// 如果会话Id存在就查询会话详情，如果不存在就生成会话Id
if (!chatId.value) {
  // 生成会话Id
  chatId.value = UUID()
} else {
  // 查询会话信息
  httpGet('api/chat/detail', {chat_id: chatId.value}).then(res => {
    roleId.value = res.data.role_id
    modelId.value = res.data.model_id
  }).catch(e => {
    console.error('获取会话详情失败：' + e.message)
  })
}

// 获取系统配置
getSystemInfo().then(res => {
  config.value = res.data
  title.value = res.data.title
}).catch(e => {
  ElMessage.error('获取系统配置失败：' + e.message)
})

const md = new MarkdownIt({
  breaks: true, // 将换行符转换为 `<br>` 标签
  html: true, // 允许 Markdown 中嵌入 HTML 标签
  linkify: true, // 自动将 URL 文本转为超链接
  typographer: true, // 启用排版优化（如引号美化、连字符转换）
}).use(emoji)

// 获取系统公告
httpGet('api/config/get?key=notice').then(res => {
  try {
    // md.render 将修剪后的 Markdown 文本转换为 HTML 字符串
    notice.value = md.render(res.data['content'])
    // 获取本地存储的旧公告数据
    const oldNotice = localStorage.getItem(noticeKey.value)
    // 如果公告有更新，则显示公告
    if (oldNotice !== notice.value && notice.value.length > 10) {
      showNotice.value = true
    }
  } catch (e) {
    console.log('处理系统公告出现错误：', e)
  }
}).catch(e => {
  ElMessage.error('获取系统公告失败：' + e.message)
})

// 获取工具函数
httpGet('api/function/list').then(res => {
  tools.value = res.data
}).catch(e => {
  showMessageError('获取工具函数失败：' + e.message)
})
// 模型列表是否可以选择
const disableModel = ref(false)
// 用户上传的文件列表
const files = ref([])
// 提示词
const prompt = ref('')
// 停止生成
const showStopGenerate = ref(false)
// 输出缓冲行，把每次接受到的消息累加到缓冲行中，就类似打字机的效果
const lineBuffer = ref('')
// 是否可以发送提示词
const canSend = ref(true)
// 是否是一条新消息：如果我们使用的是流式输出，第一次接收服务端发送过来的消息，就是新的消息，如果再次发送就不是一条新的消息，直到整个流式输出完成，就再次设置为true
const isNewMsg = ref(true)

onMounted(() => {
  resizeElement()
  initData()

  // .copy-reply 绑定在显示大模型回复消息的标签上
  // .copy-code-btn 绑定在代码框的组件上
  const clipboard = new Clipboard('.copy-reply, .copy-code-btn, .copy-prompt')
  clipboard.on('success', () => {
    ElMessage.success('复制成功！')
  })

  clipboard.on('error', () => {
    ElMessage.error('复制失败！')
  })
  // 监听浏览器窗口的尺寸变化事件，并在窗口大小改变时自动触发 resizeElement 函数，从而实现页面布局的动态调整
  window.onresize = () => resizeElement()
  // 当服务端回复消息的时候会触发 callback 回调函数，data 就是服务端返回的消息
  store.addMessageHandler('chat', (data) => {
    // 丢弃非本频道和本客户端的消息
    if (data.channel !== 'chat' || data.clientId !== getClientId()) {
      return
    }
    // 判断消息类型是否为错误
    if (data.type === 'error') {
      ElMessage.error('服务发送错误类型消息：' + data.body)
      return
    }
    // 获取应用角色信息
    const chatRole = getRoleById(roleId.value)
    // 判断是不是第一次接收消息
    if (isNewMsg.value && data.type !== 'end') {
      // 获取我们消息列表中的最后一条消息，在我们当前这个回调方法中，最后一条消息就是我们的发送的提示词
      const prePrompt = chatData.value[chatData.value.length - 1]?.content
      chatData.value.push({
        type: 'reply',
        id: randString(32),
        icon: chatRole['icon'],
        prompt: prePrompt,
        content: data.body
      })
      // 设置不是一条新的消息
      isNewMsg.value = false
      // 把接收到的消息设置到缓冲行
      lineBuffer.value = data.body
    } else if (data.type === 'end') { // 消息接收完毕
      // 把新建的会话追加到会话列表中
      if (newChatItem.value !== null) {
        newChatItem.value['title'] = tmpChatTitle.value
        newChatItem.value['chat_id'] = chatId.value
        // unshift 把元素加入到数组的头部
        chatList.value.unshift(newChatItem.value)
        newChatItem.value = null; // 只追加一次
      }
      enableInput()
      lineBuffer.value = ''; // 清空缓冲
      // 获取消息列表中最后一条消息，最后一条消息就是大模型回复的消息，因为我们在第一次接收到大模型的消息就把消息存储到消息列表中了
      const reply = chatData.value[chatData.value.length - 1]
      const prompt = chatData.value[chatData.value.length - 2]
      // 新提问的消息和回复的消息
      httpGet('api/chat/historyLastTwoMsg?chat_id=' + chatId.value).then(res => {
        const dataMsg = res.data
        dataMsg.forEach(item => {
          if (item.type === 'reply') {
            reply['created_at'] = item.created_at
            reply['tokens'] = item.tokens;
          } else if (item.type === 'prompt') {
            prompt['created_at'] = item.created_at
            prompt['tokens'] = item.tokens
          }
        })
        // nextTick 的作用是确保 reply 触发的 DOM 更新完成后再执行
        // 将聊天框的滚动条滑动到最底部
        nextTick(() => {
          document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
        })
      }).catch((e) => {
        console.log('获取消息tokens失败：' + e)
      })
      // 消息接收完成后重置新消息标记
      isNewMsg.value = true
    } else if (data.type === 'text') {
      // 累加缓冲行中的消息
      lineBuffer.value += data.body
      const reply = chatData.value[chatData.value.length - 1]
      if (reply) {
        // 把缓冲行中的数据放入到大模型回复消息的结构体中
        reply['content'] = lineBuffer.value
      }
    }
    // 将聊天框的滚动条滑动到最底部
    nextTick(() => {
      document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
      localStorage.setItem("chat_id", chatId.value)
    })
  })
})

// 离开页面触发
onUnmounted(() => {
  // 移除websocket连接监听的事件和注册的回调函数
  store.removeMessageHandler('chat')
})

// 初始化数据
// 1、加载模型
// 2、加载应用角列表
// 3、判断是否登录，如果登录就创建会话连接
// 4、获取会话列表
// 5、给输入框绑定事件
const initData = () => {
  // 加载模型
  httpGet('api/model/list').then(res => {
    models.value = res.data
    if (!modelId.value) {
      modelId.value = models.value[0].id
    }
    // 加载角色列表
    httpGet('api/app/list/user', {id: roleId.value}).then(res => {
      roles.value = res.data
      if (!roleId.value) {
        roleId.value = roles.value[0]['id']
      }
      // 如果是登录状态就创建对话连接
      checkSession().then(user => {
        loginUser.value = user
        isLogin.value = true
        newChat()
      }).catch(() => {
      })
    }).catch(e => {
      ElMessage.error('获取聊天角色失败：' + e.message)
    })
  }).catch(e => {
    ElMessage.error('加载模型失败：' + e.message)
  })

  // 获取会话列表
  httpGet('api/chat/list').then(res => {
    if (res.data) {
      chatList.value = res.data
      allChats.value = res.data
    }
  }).catch(e => {
    ElMessage.error('加载会话列表失败：' + e.message)
  })

  // 允许在输入框中粘贴文件，比如把文件复制到输入框中
  // 在inputRef指向的DOM元素上绑定paste事件监听器。当用户在该元素上执行粘贴操作（如Ctrl+V或右键粘贴）时触发回调函数
  // (event) 事件回调函数，接收ClipboardEvent对象作为参数。该对象包含剪贴板数据及操作信息
  inputRef.value.addEventListener('paste', (event) => {
    // event.clipboardData：标准API中的剪贴板数据对象，类型为DataTransfer，支持现代浏览器
    // window.clipboardData：兼容旧版IE的备用方案（如IE11及以下），但现代浏览器已废弃此属性
    const items = (event.clipboardData || window.clipboardData).items
    for (let item of items) {
      // kind 数据类型
      if (item.kind === 'file') {
        // 如果kind是file，可以用 getAsFile() 方法获取到文件
        const file = item.getAsFile()
        // 创建 Form 表单
        const formData = new FormData()
        formData.append('file', file)
        loading.value = true
        // 执行上传操作
        httpPost('api/file/upload', formData).then(res => {
          files.value.push(res.data)
          ElMessage.success({message: '上传成功', duration: 500})
          loading.value = false
        }).catch(e => {
          ElMessage.error('文件上传失败：' + e.message)
          loading.value = false
        })
        break
      }
    }
  })
}

// 根据应用角色Id获取详细信息
const getRoleById = (rid) => {
  for (let i = 0; i < roles.value.length; i++) {
    if (roles.value[i]['id'] === rid) {
      return roles.value[i]
    }
  }
  return null
}

// 根据浏览器窗口的当前高度，动态计算并设置页面中三个容器元素的高度
const resizeElement = () => {
  /* chatBoxHeight.value = window.innerHeight - 101 - 82 - 38
   mainWinHeight.value = window.innerHeight - 101
   leftBoxHeight.value = window.innerHeight - 90 - 45 - 82*/
  chatBoxHeight.value = window.innerHeight - 101 - 82 - 38
  mainWinHeight.value = window.innerHeight - 59
  leftBoxHeight.value = window.innerHeight - 90 - 100
}

// 重置会话
const _newChat = () => {
  if (isLogin.value) {
    // 生成会话Id
    chatId.value = UUID()
    // 新建会话
    newChat()
  }
}

// 新建会话
const newChat = () => {
  // 判断是否登录
  if (!isLogin.value) {
    // 显示登录框
    store.setShowLoginDialog(true)
    return
  }
  // 获取应用角色信息
  const role = getRoleById(roleId.value)
  // 如果选择的角色表示是 gpt 就显示欢迎页面信息
  showHello.value = role.key === 'gpt'
  // 如果应用角色绑定了模型就拒绝模型更改
  disableModel.value = false
  if (role.model_id > 0) {
    modelId.value = role.model_id
    disableModel.value = true
  }

  // 已有新开的会话
  if (newChatItem.value !== null && newChatItem.value['role_id'] === roles.value[0]['role_id']) {
    return;
  }

  // 获取当前聊天角色图标
  let icon = role['icon']
  // 判断当前会话Id是重新生成的还是原来就存在的会话，如果是新生成的会话，就创建新会话对象
  const isChatItem = chatList.value.some(item => item.chat_id === chatId.value)
  if (!isChatItem) {
    newChatItem.value = {
      chat_id: '',
      icon: icon,
      role_id: roleId.value,
      model_id: modelId.value,
      title: '',
      edit: false,
      removing: false
    }
  }

  showStopGenerate.value = false
  // 加载会话消息
  loadChatHistory(chatId.value)
  router.push(`/chat/${chatId.value}`)
}

// 加载会话
const loadChat = (chat) => {
  if (!isLogin.value) {
    store.setShowLoginDialog(true)
    return
  }
  // 判断当前要切换的会话Id是不是和当前选中的会话Id一致
  if (chatId.value === chat.chat_id) {
    return
  }
  newChatItem.value = null
  roleId.value = chat.role_id
  modelId.value = chat.model_id
  chatId.value = chat.chat_id
  showStopGenerate.value = false
  // 加载会话消息
  loadChatHistory(chatId.value)
  router.replace(`/chat/${chatId.value}`)
}

// 临时要修改的会话标题
const tmpChatTitle = ref('')
// 编辑会话标题
const editChatTitle = (chat) => {
  // 设置显示修改标题输入框
  chat.edit = true
  tmpChatTitle.value = chat.title
  // nextTick 的作用是确保 tmpChatTitle.value = chat.title 触发的 DOM 更新完成后再执行 document.getElementById('chat-'+chat.chat_id).focus()
  nextTick(() => {
    // 调用 DOM 元素的 focus() 方法，将页面光标移至该元素
    document.getElementById('chat-' + chat.chat_id).focus()
  })
}

// 监听修改会话标题输入框的回车事件
const titleKeyDown = (e, chat) => {
  if (e.keyCode === 13) {
    // stopPropagation()方法是一个重要的事件处理函数，它用于阻止事件在DOM树中进一步传播。这意味着，当一个元素上的事件被触发时，event.stopPropagation() 可以阻止事件向上冒泡到父元素或向下捕获到子元素。
    e.stopPropagation()
    // 修改会话标题
    editConfirm(chat)
  }
}

// 阻止事件进行冒泡到父元素或向下捕获到子元素。
const stopPropagation = (e) => {
  e.stopPropagation();
}

// 发送请求修改会话标题
const editConfirm = (chat) => {
  if (tmpChatTitle.value === '') {
    return ElMessage.error('请输入会话标题！')
  }

  if (!chat.chat_id) {
    return ElMessage.error('会话 Id 为空，请刷新页面重试！')
  }

  // 判断要修改的会话标题是否和原来的会话标题一致
  if (tmpChatTitle.value === chat.title) {
    chat.edit = false
    return
  }

  httpPost('api/chat/update', {chat_id: chat.chat_id, title: tmpChatTitle.value}).then(() => {
    chat.title = tmpChatTitle.value
    chat.edit = false
  }).catch(e => {
    ElMessage.error('修改会话标题失败：' + e.message)
  })
}

// 删除会话
const removeChat = (chat) => {
  ElMessageBox.confirm(
      `该操作会删除${chat.title}`,
      '删除聊天',
      {
        confirmButtonText: '删除',
        cancelButtonText: '取消',
        type: 'warning'
      }
  ).then(() => {
    // 发送删除会话请求
    httpGet('api/chat/remove?chat_id=' + chat.chat_id).then(() => {
      // 从会话列表中移除
      chatList.value = removeArrayItem(chatList.value, chat, (v1, v2) => {
        return v1.id === v2.id
      })
      // 如果要删除的会话是当前选中的会话，就重置会话
      if (chat.chat_id === chatId.value) {
        // 重置会话
        _newChat()
      }
    }).catch(e => {
      ElMessage.error('删除会话失败：' + e.message)
    })
  }).catch(() => {
  })
}

// 如果当前发送提示词的结果输出还没结束，就不能再发送消息，并且停止输出按钮应该显示
const disableInput = (force) => {
  // 设置当前不能发送消息
  canSend.value = false
  // 停止输出按钮是否可以显示
  showStopGenerate.value = !force
}

// 设置当前可以向大模型发送消息，并且停止输出按钮应该关闭
const enableInput = () => {
  canSend.value = true
  showStopGenerate.value = false
}

// 输入框回车和输入触发事件
/*
用户输入 "Hello\nWorld"（两行内容）：
  lineHeight 计算为 24px。
  textHeightRef 宽度同步为输入框宽度（300px），内容高度为 48px → 行数 lines = 2。
  输入框行数 row.value = 2，高度自动调整为两行。
用户按下 Ctrl+Enter：
  插入换行符，输入框显示三行内容。
用户按下单独 Enter：
  阻止换行，提交消息，清空输入框。
 */
const onInput = (e) => {
  // 根据输入的内容自动计算输入框行数

  // 获取输入框的行高（单位：像素）。
  // window.getComputedStyle(element)：获取元素最终计算后的 CSS 样式（包括继承和外联样式）
  // lineHeight：行高属性值（如 24px 或 1.5）
  // parseFloat()：将字符串转换为浮点数（如 "24px" → 24）
  // 若输入框 CSS 定义为 line-height: 24px;，则 lineHeight 值为 24。
  const lineHeight = parseFloat(window.getComputedStyle(inputRef.value).lineHeight)
  // 将一个隐藏的辅助元素 textHeightRef 的宽度设置为输入框的实际宽度，用于后续计算内容高度
  // clientWidth：元素的内部宽度（不含滚动条和边框）。
  // style.width：直接设置元素的 CSS 宽度。
  // 输入框宽度为 300px，则 textHeightRef 的宽度同步为 300px，确保换行计算准确。
  textHeightRef.value.style.width = inputRef.value.clientWidth + 'px'
  // 根据辅助元素的内容高度和行高，计算当前输入内容的行数。
  // clientHeight：元素内容区域的高度（含内边距，不含边框和外边距）。
  // Math.floor()：向下取整，避免小数行数。
  // 若 textHeightRef 内容高度为 96px，行高为 24px，则 lines = 4。
  const lines = Math.floor(textHeightRef.value.clientHeight / lineHeight)
  // 当输入内容超出可视区域时，自动滚动到输入框底部，确保最新输入可见。
  // scrollHeight：元素内容的总高度（包括被隐藏的部分）。
  // scrollTo(x, y)：滚动到指定坐标。
  // 多行输入时，输入框不会停留在中间，始终显示最后一行。
  inputRef.value.scrollTo(0, inputRef.value.scrollHeight)

  // 用户输入一段长文本导致行数达到 10 行，但输入框仅显示 7 行，多余内容通过滚动查看。
  // 内容长度 < 10：固定 1 行（避免空输入框过高）
  if (prompt.value.length < 10) {
    row.value = 1
  } else if (lines <= 7) { // 行数 ≤ 7：按实际行数显示。
    row.value = lines
  } else {
    row.value = 7 // 行数 > 7：限制为 7 行（防止输入框无限增高）
  }

  // 输入回车自动提交
  if (e.keyCode === 13) {
    // Ctrl + Enter 换行
    if (e.ctrlKey) {
      prompt.value += "\n"
      return
    }
    // 阻止浏览器默认行为（如输入框换行）
    e.preventDefault()
    sendMessage()
  }
}

// 自动填充 Prompt
const autofillPrompt = (text) => {
  prompt.value = text
  // 光标焦点聚集
  inputRef.value.focus()
  // 发送消息
  sendMessage()
}

// 发送消息
const sendMessage = () => {
  // 判断是否登录
  if (!isLogin.value) {
    store.setShowLoginDialog(true)
    return
  }
  // 判断websocket连接是否打开
  if (store.socket.conn.readyState !== WebSocket.OPEN) {
    ElMessage.warning('连接断开，正在重连...')
    return
  }

  // 判断当前是否可以发送消息，如果上一次的回答还没完结就不能再次发送消息
  if (canSend.value === false) {
    ElMessage.warning('AI 正在作答中，请稍后...')
    return
  }

  if (prompt.value.trim().length === 0 || canSend.value === false) {
    ElMessage.warning('请输入要发送的消息！')
    return
  }

  // 如果携带了文件，则加上文件地址
  let content = prompt.value
  if (files.value.length === 1) {
    content += files.value.map(file => file.url).join(' ')
  } else if (files.value.length > 1) {
    showMessageError('当前只支持上传一个文件！')
    return
  }

  // 向消息列表中追加消息
  chatData.value.push({
    type: 'prompt',
    id: randString(32),
    icon: loginUser.value.avatar,
    content: content,
    model: getModelValue(modelId.value),
    create_at: new Date().getTime() / 1000
  })

  // nextTick 的作用是确保 chatData 触发的 DOM 更新完成后再执行
  nextTick(() => {
    // scrollTo(x, y)：滚动到指定坐标。
    // scrollHeight：元素内容的总高度（包括被隐藏的部分）。
    document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
  })

  showHello.value = false
  disableInput(false)
  const msgBody = {
    channel: 'chat',
    type: 'text',
    body: {
      role_id: roleId.value, // 应用角色Id
      model_id: modelId.value, // 模型Id
      chat_id: chatId.value, // 会话Id
      content: content, // 提示词
      tools: toolSelected.value, // 函数工具列表
      stream: stream.value // 是否流式输出
    }
  }
  console.log("发送消息：" + JSON.stringify(msgBody))
  // 发送消息
  store.socket.conn.send(JSON.stringify(msgBody))
  tmpChatTitle.value = content
  prompt.value = ''
  files.value = []
  row.value = 1
  return true
}

// 清空所有会话
const clearAllChats = () => {
  ElMessageBox.confirm(
      '清除所有对话?此操作不可撤销！',
      '警告',
      {
        confirmButtonText: '删除对话',
        cancelButtonText: '取消',
        dangerouslyUseHTMLString: true,
        showClose: true,
        closeOnClickModal: false,
        center: true,
      }
  ).then(() => {
    httpGet('api/chat/clear').then(() => {
      ElMessage.success('操作成功！')
      chatData.value = []
      chatList.value = []
      newChat()
    }).catch(e => {
      ElMessage.error('操作失败：' + e.message)
    })
  }).catch(() => {
  })
}

// 加载会话历史消息
const loadChatHistory = (chatId) => {
  chatData.value = []
  loading.value = true
  httpGet('api/chat/history?chat_id=' + chatId).then(res => {
    loading.value = false
    const data = res.data
    // 如果会话没有历史消息，就加载应用角色的打招呼消息
    if ((!data || data.length === 0) && chatData.value.length === 0) {
      // 获取应用角色信息
      const _role = getRoleById(roleId.value)
      chatData.value.push({
        chat_id: chatId,
        role_id: roleId.value,
        type: 'reply',
        id: randString(32),
        icon: _role['icon'],
        content: _role['hello_msg']
      })
      return
    }

    showHello.value = false
    for (let i = 0; i < data.length; i++) {
      // 如果消息类型是大模型回复的消息并且 i > 0
      // 我们就给大模型回复的消息对象中设置我们请求的提示词。
      /*
        我们从后端获取到的消息是
        const data = [
          { type: 'user', content: '你好' },                // 索引 0
          { type: 'reply', content: '您好，有什么可以帮您？' }, // 索引 1
          { type: 'user', content: '推荐一部电影' },          // 索引 2
          { type: 'reply', content: '《肖申克的救赎》' }       // 索引 3
        ];
        经过这个 if 之后，每个大模型回复的消息对象中就多了一个 prompt 属性，这个就是我们提问的提示词
        [
          { type: 'user', content: '你好' },
          { type: 'reply', content: '您好...', prompt: '你好' },
          { type: 'user', content: '推荐一部电影' },
          { type: 'reply', content: '《肖申克...》', prompt: '推荐一部电影' }
        ]
        这个作用就是如果我们在页面点击重新生成按钮就可以直接拿到我们的提示词重新生成
       */
      if (data[i].type === 'reply' && i > 0) {
        data[i].prompt = data[i - 1].content
      }
      chatData.value.push(data[i])
    }
    // nextTick 的作用是确保 chatData 触发的 DOM 更新完成后再执行
    nextTick(() => {
      console.log("chatData.value:", chatData.value)
      // scrollTo(x, y)：滚动到指定坐标。
      // scrollHeight：元素内容的总高度（包括被隐藏的部分）。
      document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
    })
  }).catch(e => {
    // TODO: 显示重新加载按钮
    ElMessage.error('加载会话聊天记录失败：' + e.message)
  })
}

// 停止大模型生成
const stopGenerate = () => {
  // 设置停止输入按钮为隐藏
  showStopGenerate.value = false
  httpGet('api/chat/stop?session_id=' + getClientId()).then(() => {
    enableInput()
  })
}

// 重新生成
const reGenerate = (prompt) => {
  disableInput(false)
  const text = '重新回答下述问题：' + prompt
  // 在我们历史消息集合中追加重新提问的消息
  chatData.value.push({
    type: 'prompt',
    id: randString(32),
    icon: loginUser.value.avatar,
    content: text
  })
  // 发送消息
  store.socket.conn.send(JSON.stringify({
    channel: 'chat',
    type: 'text',
    body: {
      role_id: roleId.value, // 应用角色Id
      model_id: modelId.value, // 模型Id
      chat_id: chatId.value, // 会话Id
      content: text, // 提示词
      tools: toolSelected.value, // 函数工具列表
      stream: stream.value // 是否流式输出
    }
  }))
}

// 搜索会话输入的内容
const chatName = ref('')
// 搜索会话
const searchChat = (e) => {
  // 如果输入的内容为空就显示所有的会话
  if (chatName.value === '') {
    chatList.value = allChats.value
    return
  }
  // 判断键盘码是不是回车
  if (e.keyCode === 13) {
    const items = []
    for (let i = 0; i < allChats.value.length; i++) {
      if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
        items.push(allChats.value[i])
      }
    }
    chatList.value = items
  }
}

// 导出会话，使用新的窗口显示会话
const shareChat = (chat) => {
  if (!chat.chat_id) {
    return ElMessage.error('请先选中一个会话')
  }
  const url = location.protocol + '//' + location.host + '/chat/export?chat_id=' + chat.chat_id
  // 在浏览器打开新的标签页
  window.open(url, '_blank')
}

// 获取模型的value值
const getModelValue = (modelId) => {
  for (let i = 0; i < models.value.length; i++) {
    if (models.value[i].id === modelId) {
      return models.value[i].value
    }
  }
  return ''
}

// 关闭公告弹框，并且把公告信息存储到本地
const notShow = () => {
  localStorage.setItem(noticeKey.value, notice.value)
  showNotice.value = false
}

// 插入文件
const insertFile = (file) => {
  files.value.push(file)
}

// 移除文件
const removeFile = (file) => {
  files.value = removeArrayItem(files.value, file, (v1, v2) => v1.url === v2.url)
}

// 实时语言对话
// 是否显示语音弹框
const showConversationDialog = ref(false)
const frameLoaded = ref(false)
const realtimeChat = () => {
  // 判断是否登录
  if (!isLogin.value) {
    // 显示登录弹框
    store.setShowLoginDialog(true)
    return
  }
  showLoading('正在连接...')
  httpPost('api/realtime/voice').then(res => {
    voiceChatUrl.value = res.data
    showConversationDialog.value = true
    closeLoading()
  }).catch(e => {
    showMessageError('连接失败：' + e.message)
    closeLoading()
  })
}

</script>

<template>
  <div class="chat-page">
    <el-container>
      <!-- 会话列表 -->
      <el-aside>
        <div class="media-page">
          <!-- 新建对话按钮 -->
          <el-button @click="_newChat" type="primary" class="newChat">
            <el-icon style="margin-right: 5px">
              <Plus/>
            </el-icon>
            新建对话
          </el-button>
          <!-- 搜索对话输入框 -->
          <div class="search-box">
            <el-input v-model="chatName" placeholder="搜索会话" @keyup="searchChat($event)" style=""
                      class="search-input">
              <template #prefix>
                <el-icon class="el-input__icon">
                  <Search/>
                </el-icon>
              </template>
            </el-input>
          </div>
          <!-- 会话列表 -->
          <el-scrollbar :height="chatBoxHeight">
            <div class="content">
              <el-row v-for="chat in chatList" :key="chat.chat_id">
                <!-- chat.chat_id === chatId 判断当前的会话是不是选中的 -->
                <div :class="chat.chat_id === chatId ? 'chat-list-item active' : 'chat-list-item'"
                     @click="loadChat(chat)">
                  <!-- 显示会话图标 -->
                  <el-image :src="chat.icon" class="avatar"/>
                  <!-- 如果修改会话标题就显示输入框 -->
                  <span class="chat-title-input" v-if="chat.edit">
                    <!-- keydown键盘事件 blur在失去焦点的元素上触发 -->
                    <el-input v-model="tmpChatTitle" size="small" @keydown="titleKeyDown($event,chat)"
                              :id="'chat-'+chat.chat_id" @blur="editConfirm(chat)" @click="stopPropagation($event)"
                              placeholder="请输入标题"/>
                  </span>
                  <!-- 如果不修改会话标题，就正常显示会话标题 -->
                  <span v-else class="chat-title">{{ chat.title }}</span>
                  <!-- 操作按钮 -->
                  <span class="chat-opt">
                  <el-dropdown trigger="click">
                    <span class="el-dropdown-link" @click="stopPropagation($event)">
                      <el-icon><More/></el-icon>
                    </span>
                    <template #dropdown>
                      <el-dropdown-menu>
                        <el-dropdown-item :icon="Edit" @click="editChatTitle(chat)">重命名</el-dropdown-item>
                        <el-dropdown-item :icon="Delete"
                                          style="--el-text-color-regular: var(--el-color-danger);
                                          --el-dropdown-menuItem-hover-fill:#F8E1DE;
                                          --el-dropdown-menuItem-hover-color: var(--el-color-danger)"
                                          @click="removeChat(chat)">删除
                        </el-dropdown-item>
                        <el-dropdown-item :icon="Share" @click="shareChat(chat)">分享</el-dropdown-item>
                      </el-dropdown-menu>
                    </template>
                  </el-dropdown>
                </span>
                </div>
              </el-row>
            </div>
          </el-scrollbar>
        </div>
        <div class="tool-box">
          <el-button type="primary" size="small" @click="clearAllChats">
            <i class="iconfont icon-clear"></i> 清空聊天记录
          </el-button>
        </div>
      </el-aside>
      <!-- 聊天界面 -->
      <el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
        <div class="chat-container">
          <div class="chat-config">
            <!--  显示角色下拉选项框
                  filterable：启用搜索过滤功能 change：选择新选项并确认后触发
            -->
            <el-select v-model="roleId" filterable placeholder="角色" @change="_newChat" class="role-select"
                       style="width:150px">
              <el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id">
                <div class="role-option">
                  <el-image :src="item.icon"></el-image>
                  <span>{{ item.name }}</span>
                </div>
              </el-option>
            </el-select>
            <!-- 显示模型下拉选项框 -->
            <el-select v-model="modelId" filterable placeholder="模型" @change="_newChat" :disabled="disableModel"
                       style="width:150px">
              <el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id">
                <span>{{ item.name }}</span>
                <el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">
                  {{ item.power }}算力
                </el-tag>
              </el-option>
            </el-select>
            <!-- 插件工具显示 -->
            <div class="flex-center">
              <el-dropdown :hide-on-click="false" trigger="click">
                <span class="setting"><i class="iconfont icon-plugin"></i></span>
                <template #dropdown>
                  <el-dropdown-menu class="tools-dropdown">
                    <el-checkbox-group v-model="toolSelected">
                      <el-dropdown-item v-for="item in tools" :key="item.id">
                        <el-checkbox :value="item.id" :label="item.label"/>
                        <el-tooltip :content="item.description" placement="right">
                          <el-icon>
                            <InfoFilled/>
                          </el-icon>
                        </el-tooltip>
                      </el-dropdown-item>
                    </el-checkbox-group>
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
            </div>
            <!-- 聊天配置控制按钮 -->
            <span class="setting" @click="showChatSetting = true">
              <i class="iconfont icon-config"></i>
            </span>
          </div>
          <div>
            <div id="container" :style="{height: mainWinHeight + 'px'}">
              <div class="chat-box" id="chat-box" :style="{height: chatBoxHeight + 'px'}">
                <!-- 显示欢迎页面 -->
                <div v-if="showHello">
                  <welcome @send="autofillPrompt"/>
                </div>
                <!-- 显示历史消息 -->
                <div v-for="item in chatData" :key="item.id" v-else>
                  <chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle"/>
                  <chat-reply v-else-if="item.type === 'reply'" :data="item" @regen="reGenerate" :read-only="false"
                              :list-style="listStyle"/>
                </div>
                <back-top :right="30" :bottom="155"/>
              </div>
              <!-- 提示词输入框 -->
              <div class="input-box">
                <div class="input-box-inner">
                  <!-- 提示词输入框 -->
                  <div class="input-body">
                    <div ref="textHeightRef" class="hide-div">{{ prompt }}</div>
                    <div class="input-border">
                      <!-- 输入框 -->
                      <div class="input-inner">
                        <div class="file-list" v-if="files.length > 0">
                          <!-- :files="files" 用户上传的文件列表 @remove-file="removeFile" 移除文件触发的事件  -->
                          <file-list :files="files" @remove-file="removeFile"/>
                        </div>
                        <textarea ref="inputRef" class="prompt-input" :rows="row" v-model="prompt" @keydown="onInput"
                                  @input="onInput" placeholder="按 Enter 键发送消息，使用 Ctrl + Enter 换行"
                                  autofocus>
                        </textarea>
                      </div>
                      <div class="flex-between">
                        <div class="flex little-btns">
                          <!-- 实时语音对话 -->
                          <span class="tool-item-btn" @click="realtimeChat">
                            <el-tooltip class="box-item" effect="dark" :content="'实时语音对话，每次消耗' + config.advance_voice_power + '算力'">
                               <i class="iconfont icon-mic-bold"></i>
                            </el-tooltip>
                          </span>
                          <!-- 上传附件 -->
                          <span class="tool-item-btn">
                             <el-tooltip class="box-item" effect="dark" content="上传附件">
                               <file-select :user-id="loginUser?.id" @selected="insertFile"/>
                            </el-tooltip>
                          </span>
                        </div>
                        <div class="flex little-btns">
                          <!-- 发送按钮和停止生成按钮 -->
                          <span class="send-btn tool-item-btn">
                            <el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
                              <el-icon>
                                <VideoPause/>
                              </el-icon>
                            </el-button>
                            <el-button v-else @click="sendMessage" style="color: #754ff6">
                              <el-tooltip class="box-item" effect="dark" content="发送">
                                <el-icon><Promotion /></el-icon>
                              </el-tooltip>
                            </el-button>
                          </span>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </el-main>
    </el-container>
    <!-- 公共弹框 -->
    <el-dialog v-model="showNotice" :show-close="true" class="notice-dialog" title="网站公告">
      <div class="notice p-4">
        <div v-html="notice"></div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="notShow" type="primary" plain>我知道了，不再显示</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- 设置组件 -->
    <ChatSetting :show="showChatSetting" @hide="showChatSetting = false"/>
    <el-dialog v-model="showConversationDialog" title="实时语音通话" :fullscreen="true">
      <div v-loading="!frameLoaded">
        <iframe style="width: 100%; height: calc(100vh - 100px); border: none"
                :src="voiceChatUrl"
                @load="frameLoaded = true"
                allow="microphone *;camera *;">
        </iframe>
      </div>
    </el-dialog>
  </div>
</template>

<style scoped lang="stylus">
@import "../assets/css/chat-plus.styl"
</style>

<style lang="stylus">
.notice-dialog {
  .el-dialog__header {
    padding-bottom 0
  }

  .el-dialog__body {
    padding 0 20px

    ol, ul {
      padding-left 10px
    }

    ol {
      list-style decimal-leading-zero
      padding-left 20px
    }

    ul {
      list-style disc
    }
  }
}

.input-container {
  .el-textarea {
    .el-textarea__inner {
      padding-right 40px
    }
  }
}
</style>